Skip to content

feat(ir)!: give parameters and enum members stable IDs - #811

Open
fuad-daoud wants to merge 3 commits into
mainfrom
feat/ir-param-member-ids
Open

fuad-daoud wants to merge 3 commits into
mainfrom
feat/ir-param-member-ids

Conversation

@fuad-daoud

Copy link
Copy Markdown
Collaborator

Summary

Closes #596. Part of #255: item 1, enum-member identity. Items 2–4 have no code yet; their decisions are recorded on #255.

Replaces #807. That PR conflicted with #801, which landed operation-group identity and declared ID namespaces while #807 was in review. This PR is the part #801 does not cover, rebuilt on #801's framework.

Two named things in the IR were referenced only by their name, and names are presentation (invariant 3). This gives each a synthetic ID, moves every reference onto it, and registers both new kinds in Document.IDSpaces.

Parameters: ParamID (#596)

OpenAPI identifies a parameter by (name, in), so id in the query and id in the path are both legal on one operation. The IR referenced parameters by name, so their two bindings could not say which one they bound.

  • Parameter.ID is param/openapi/<operation mount pointer>/parameters/<name>/<in>.
    • The name is one RFC 6901-escaped token, through the same ids.Ptr escaping feat(ir)!: identify operation groups and declare ID namespaces #801 uses.
    • The ID is scoped by the operation's mount, not its declaration. A path-item parameter is copied into every operation on the item, and a $ref'd path item can be mounted at several paths.
    • No list index is used, so reordering parameters leaves every ID unchanged.
  • References. HTTPParamBinding.Param, Pagination.InputCursor/InputLimit (ParamPath.Param) and Idempotency.TokenParam now hold a ParamID.
  • pass/validate rejects any of them naming a parameter its own operation does not declare (pass/param-binding-mismatch). An ID nobody declares is left to the dangling-reference check, so it is reported once.
  • irverify:

Enum members: EnumMemberID (#255, item 1)

emitter-design §4.12 resolves naming collisions in a table keyed by IR ID. The commonest collision, enum members canonicalizing to the same words, had no ID to key.

  • EnumMember.ID is e/<enum's namespace>/<enum's path>/<key>.
    • The key is derived from the member's wire value, never its position, so reordering or inserting members changes no existing ID.
    • The key is kind-tagged (s: string, y: symbol, n: canonical number, b: bool, x: bytes), so 1 and "1" differ.
  • Namespace. The member lives in its enum's namespace: openapi for a component, anon for an inline enum. It is derived from the enum's pointer the way ids.ForPointer is, so no compile.Space is built from data (feat(ir)!: identify operation groups and declare ID namespaces #801's archtest rule).
  • A repeated value keeps every member. The second takes a #2 suffix, and the compiler warns openapi/duplicate-enum-value.
  • Checks.
    • pass/duplicate-enum-member-id reports a repeated ID within one enum.
    • irverify reports a member ID outside its enum's namespace or path (ir/member-id-scope).
    • §4.12 names EnumMember.ID as the collision key.

Deliberately not changed

Breaking

  • IRVersion moves from 0.7.0 to 0.8.0. §2.1 bumps once where a change lands on main, and 0.7.0 already landed with feat(ir)!: identify operation groups and declare ID namespaces #801. CompatibleVersion is exact, so a stored 0.7.0 document is refused.
  • HTTPParamBinding.Param, ParamPath.Param and Idempotency.TokenParam change from a parameter name to a ParamID.
  • Parameter and EnumMember gain a required id. Document.IDSpaces gains the param and e kinds.
  • All goldens are regenerated.

Test plan

  • make gate passes on this branch (base e0c9762), including 100% statement coverage, golangci-lint v2.14.0, fuzz and bench smoke.

  • Every new test was checked by planting the defect it guards against, watching it go red, then reverting. Highlights:

    Planted defect Went red
    declaration pointer instead of mount TestParams_PathItemParamIsScopedPerOperation, param-identity, component-reuse
    in dropped from the ID TestParams_SameNameInTwoLocationsGetsDistinctIDs
    list index in the ID TestParams_IDsFollowNameNotPosition
    binding set from the name again param-identity
    foreign IDs not rejected TestValidate_ParamReferences_OtherOperationsParamIsRejected
    param or e missing from IDSpaces 143 and 123 corpus failures (ir/id-space-undeclared)
    member key without its kind tag TestEnumMemberKey_IsInjectiveAcrossKinds
    key from the canonical name, or from the index the shared-name and reorder tests
    member always in openapi the anon-namespace case
    scope check permissive TestVerify_MemberIDOutsideItsEnumIsAViolation
  • New conformance fixtures:

    • param-identity: id in query and path; a path-item parameter on GET and POST; a $ref'd path item mounted at two paths, declared in the order that would expose a declaration-pointer ID.
    • enum-member-identity: [a-b, a_b, "A B", "a/b", "a#b", a-b], [-1, 1, 0] and an inline enum.
  • The goldens are not a no-op regeneration. Deleting the query id or the member a_b from its fixture reddens the suite without -update.

  • The harness reports ok on both new fixtures.

  • Probes with morphic compile/validate:

    • two distinct IDs for the id query/path pair, each binding naming its own;
    • four distinct parameter IDs for a path item mounted twice with get and delete;
    • six distinct member IDs and one duplicate warning for the enum above.

    Both documents validate with exit 0.

  • A duplicate (name, in) on one operation is already an error diagnostic (validation-operation-parameters), so two parameters can never share one ID.

🤖 Generated with Claude Code

https://claude.ai/code/session_016MQFAkYBVaqgSxGwe9ZJSn

fuad-daoud and others added 3 commits October 10, 2026 10:48
A parameter was named by its name, which one operation can repeat: id in
the path and id in the query are two parameters, yet a binding, a
pagination input and an idempotency token each pointed at "id".

Parameter gains ID, `param/<space>/<operation mount>/parameters/<name>/<in>`,
built on the ID framework: the name is one escaped RFC 6901 token, the
mount scopes it per operation, and no list index is used. A path-item
parameter copied into two operations, or a path item mounted from two
paths, gets an ID per mount. pass.Validate now joins bindings to
Operation.Params by ID and rejects a binding naming another operation's
parameter.

Breaking: HTTPParamBinding.Param, ParamPath.Param and
Idempotency.TokenParam change from a parameter name to a ParamID, and
Parameter gains a required "id". IRVersion moves to 0.8.0; 0.7.0
documents are no longer read.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A binding is checked against the parameters of the operation it sits on,
but Pagination.InputCursor, Pagination.InputLimit and
Idempotency.TokenParam only had to name some parameter in the document.
Now that parameters have IDs, another operation's parameter is a
well-formed reference that nothing rejected, and an emitter would read a
cursor off the wrong operation.

pass.Validate reports such a reference as pass/param-binding-mismatch.
An empty ID and an ID no operation declares are left to the checks that
already own them, so each defect is reported once.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An enum member was a name and a value. Two members can render from the
same words (a-b and a_b), and reordering the list moved every position, so
a consumer had nothing stable to key a member by and an emitter resolving
a naming collision had to key it by position.

EnumMember gains ID, `e/<enum's namespace>/<enum pointer>/<key>`. The
namespace is the one the enum's own TypeID lives in, so a member sits under
the type that declares it. The key is the value, tagged by kind and escaped
as one RFC 6901 token, never the name or the position; a value held twice
keeps every member, and the repeats take #2, #3 and are reported as
openapi/duplicate-enum-value. irverify holds a member under its enum's
namespace and path (ir/member-id-scope) and pass.Validate rejects a repeat
within one enum.

Breaking: EnumMember gains a required "id", and the "e" kind joins
IDSpaces. This extends the 0.8.0 generation, so IRVersion is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ir: parameters are bound by name, so same-named parameters in different locations are ambiguous

1 participant